﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Net.Sockets;
using System.Collections;

namespace LPT_5.HTTPSocket
{
    class Socket
    {
        private System.Net.Sockets.Socket msSock = null;
        private String msHost = null;
        private int miPort = 80;
        private Hashtable mhHeaders=null;

        public Socket(String sHost, int iPort)
        {
            msHost = sHost;
            miPort = iPort;
            
            //Initialise Standard Headers
            mhHeaders = new Hashtable();
            AddHeader("Connection","Close"); //We wont need to do any pipelining
            AddHeader("Host", sHost);
        }

        public void AddHeader(String sKey, String sValue)
        {
            if(!mhHeaders.ContainsKey(sKey))
                mhHeaders.Add(sKey,sValue);
        }

        private void RemoveHeader(String sKey)
        {
            if(mhHeaders.ContainsKey(sKey))
                mhHeaders.Remove(sKey);
        }

        public String GetURL(String sURL)
        {
            byte[] bRaw = GetRaw(sURL);
            if (bRaw == null)
                return null;

            return System.Text.Encoding.ASCII.GetString(bRaw);
        }

        public byte[] GetRaw(String sURL)
        {
            msSock = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            msSock.Connect(msHost, miPort);
            String sReq = "GET " + sURL + " HTTP/1.1";
            sReq = TagHeaders(sReq);
            msSock.Send(System.Text.Encoding.ASCII.GetBytes(sReq));

            Hashtable htHeadParams = new Hashtable();
            if(ReceiveHeader(ref htHeadParams) == 200) //If we got the data with no problems
            {
                int iLen = Convert.ToInt32((String)htHeadParams["Content-Length"], 10);
                byte[] bBuff = new byte[iLen];
                if (htHeadParams.ContainsKey("Content-Length"))
                {
                    int iOffs = 0;
                    while (iOffs < iLen)
                    {
                        iOffs += msSock.Receive(bBuff, iOffs, iLen - iOffs, SocketFlags.None);
                    }
                }
                msSock.Close();

                //TODO: Cannot automatically handle anything other than 200 OK.
                //Think about implementing a way to at least handle 302 MOVED.

                return bBuff;
            }
            return null;
        }

        private String TagHeaders(String sReq)
        {
            IDictionaryEnumerator deEnumer = mhHeaders.GetEnumerator();
            while(deEnumer.MoveNext())
            {
                sReq += "\r\n" + deEnumer.Key + ": " + deEnumer.Value;
            }
            return sReq + "\r\n\r\n";
        }

        private int ReceiveHeader(ref Hashtable htHash)
        {
            String sHead = null;
            byte[] buff = new byte[1];

            /*
             We dont know the size of the header, but we do know that header and
             body are separated by a double newline, so read a byte at a time until
             we receive double newline.
            */
            do
            {
                msSock.Receive(buff, 0, 1, SocketFlags.None);
                sHead += (char)buff[0];
            }
            while (!sHead.Contains("\r\n\r\n"));
            int iResult = ProcessHeaders(sHead,ref htHash);
            ExtractCookies(htHash);

            return iResult;
        }

        private int ProcessHeaders(String sHead, ref Hashtable htHash)
        {
            //Split wont work with \r\n, so just regex away the carriage return
            String sToSplit = Regex.Replace(sHead,"\r","");
            String[] sParams = sToSplit.Split('\n');

            foreach (String sItem in sParams)
            {
                String[] sKeyVal = sItem.Split(':');
                if(sKeyVal.Length >= 2)
                {
                    String sKey = sKeyVal[0].Trim();
                    String sVal = sKeyVal[1].Trim();
                    if (htHash.ContainsKey(sKey))
                        htHash[sKey] += ',' + sVal; //Group multiple headers
                    else
                        htHash.Add(sKey, sVal);
                }
            }

            String[] sResultParams = sParams[0].Split(' ');
            return Convert.ToInt32(sResultParams[1]); //Return the HTTP Response Code
        }

        private void ExtractCookies(Hashtable htHeaders)
        {
            if(htHeaders.ContainsKey("Set-Cookie"))
            {
                String sFinalValue = null;
                String sCookieGroup = (String)htHeaders["Set-Cookie"];
                String[] sCookies = sCookieGroup.Split(',');
                for (int i = 0; i < sCookies.Length; ++i )
                {
                    String[] sParams = sCookies[i].Split(';');
                    //The key-value pair is always the first entry
                    if(i > 0) sFinalValue += "; ";
                    sFinalValue += sParams[0];
                }

                if (sFinalValue != null)
                {
                    AddHeader("Cookie", sFinalValue);

                    //If we are receiving headers then we should have received
                    //an auth token, which means we dont have to send our username
                    //and pass with every call. We could still do this, but as BASIC AUTH
                    //uses base64, it is best not to be sending the user & password every time
                    //RemoveHeader("Authorization");
                }
            }
        }
    }
}
